﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.ServiceModel;
using ProLinq.Toolkit;
using ProLinq.Wcf.Configuration;
using ProLinq.Wcf.Linq.Providers;

namespace ProLinq.Wcf
{
	/// <summary>
	/// Proxy class for calling IQueryable methods
	/// </summary>
	/// <typeparam name="T"></typeparam>
	internal class QueryableServiceProxy<T> : RealProxy
	{
		#region Fields

		private readonly Settings _settings;
		private readonly T _source;

		#endregion

		#region Constructors

		public QueryableServiceProxy(T source)
			: base(typeof (T))
		{
			this._source = source;
			this._settings = new Settings();
		}

		public QueryableServiceProxy(T source, Settings settings)
			: base(typeof (T))
		{
			this._source = source;
			this._settings = settings;
		}

		#endregion

		#region Methods

		private Object CallMethod(MethodInfo method, Object[] args)
		{
			Func<Object> defaultCall = () => method.Invoke(this._source, args);

			// source WCF channel will return QueryType<T> for IQueryable<T>
			if (typeof (IQueryable).IsAssignableFrom(method.ReturnType))
			{
				var queryType = typeof (Query<>);
				if (method.ReturnType.IsGenericType)
				{
					queryType = queryType.MakeGenericType(method.ReturnType.GenericTypeArguments);
				}
				else
				{
					queryType = queryType.MakeGenericType(typeof (Object));
				}

				var methodCall = Expression.Call(
					Expression.Constant(null, method.DeclaringType), 
					method, 
					args.Select(a => Expression.Constant(a)));

				return Activator.CreateInstance(queryType,
				                                new ServiceQueryProvider(methodCall, (IContextChannel) this._source, defaultCall,
				                                                         this._settings.ExpressionSerializer));
			}

			// invoke default
			return defaultCall();
		}

		/// <summary>
		/// Searches for a method in the interface inheritance chain.
		/// </summary>
		/// <param name="methodName"></param>
		/// <param name="parameterTypes"></param>
		/// <returns></returns>
		private MethodInfo FindInterfaceMethod(Type type, String methodName, Type[] parameterTypes)
		{
			var method = type.GetMethod(methodName, parameterTypes);
			if (method != null)
				return method;

			foreach (var t in type.GetInterfaces())
			{
				method = FindInterfaceMethod(t, methodName, parameterTypes);
				if (method != null)
					return method;
			}

			return null;
		}

		#endregion

		#region Overrides

		public override IMessage Invoke(IMessage msg)
		{
			var methodName = (String) msg.Properties["__MethodName"];
			var parameterTypes = (Type[]) msg.Properties["__MethodSignature"];
			var method = FindInterfaceMethod(typeof(T), methodName, parameterTypes);
			var parameters = (Object[]) msg.Properties["__Args"];

			try
			{
				var result = this.CallMethod(method, parameters);
				return new ReturnMessage(result, parameters, parameters.Length, null, (IMethodCallMessage)msg);
			}
			catch (TargetInvocationException exc)
			{
				if (exc.InnerException != null)
					throw exc.InnerException;
				throw;
			}
		}

		#endregion
	}
}